01.1 精通自定义 View 之绘图基础——基本图形绘制

返回自定义 View 目录

1.1.1 概述

画图需要两个工具:纸和笔。在 Android 中,Paint 类就是画笔,而 Canvas 类就是纸,在这里叫作画布。

凡是跟画笔设置相关的,比如画笔大小、粗细、画笔颜色、透明度、字体的样式等,都在 Paint 类里设置;同样,凡是要画出成品的东西,比如圆形、矩形、文字等,都调用 Canvas 类里的函数生成。

示例:
效果图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class TestView extends View {
private Paint mPaint;
public TestView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
// 设置画笔
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(50);
}
@Override
protected void onDraw(Canvas canvas) {
// 画圆
canvas.drawCircle(200, 200, 150, mPaint);
}
}

直接在主布局中使用自定义控件

1
2
3
4
5
6
7
8
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.xxt.xtest.TestView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>

在 onDraw() 函数中不能创建变量。因为当需要重绘时就会调用 onDraw() 函数,这样会导致变量一直被重复创建,会引起频繁的程序 GC (回收内存),进而引起程序卡顿。一般在构造函数中创建变量。

1.1.2 画笔的基本设置

1. setAntiAlias()

1
void setAntiAlias(boolean aa)

表示是否打开抗锯齿功能。抗锯齿是依赖算法的,一般在绘制不规则的图形时使用,比如圆形、文字等。在绘制棱角分明的图像时,比如一个矩形、一张位图,是不需要打开抗锯齿功能的。

示例:
Anti Alias & No Anti Alias

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class TestView extends View {
private Paint mPaint1, mPaint2;
public TestView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mPaint1 = new Paint();
mPaint1.setColor(Color.RED);
mPaint1.setStyle(Paint.Style.FILL);
mPaint1.setAntiAlias(true);
mPaint1.setStrokeWidth(50);
mPaint2 = new Paint();
mPaint2.setColor(Color.RED);
mPaint2.setStyle(Paint.Style.FILL);
mPaint2.setStrokeWidth(50);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawCircle(200, 200, 150, mPaint1);
canvas.drawCircle(540, 200, 150, mPaint2);
}
}

2. setColor()

1
void setColor(int color)

设置画笔颜色。一个颜色值是由红、绿、蓝三色合成出来的,所以,参数 color 只能取 8 位的 0xAARRGGBB 样式颜色值。其中:

  • A 代表透明度(Alpha),取值范围是 0~255(对应十六进制的 0x00~0xFF),取值越小,透明度越高,图像也就越透明。当取 0 时,图像完全不可见。
  • R 代表红色值(Red),取值范围是 0~255(对应十六进制的 0x00~0xFF),取值越小,红色越少。当取 0 时,表示红色完全不可见;当取 255 时,红色完全显示。
  • G 代表绿色值(Green),取值范围是 0~255(对应十六进制的 0x00~0xFF),取值越小,绿色越少。当取 0 时,表示绿色完全不可见;当取 255 时,绿色完全显示。
  • B 代表蓝色值(Blue),取值范围是 0~255(对应十六进制的 0x00~0xFF),取值越小,蓝色越少。当取 0 时,表示蓝色完全不可见;当取 255 时,蓝色完全显示。

示例:
效果图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class TestView extends View {
private Paint mPaint1, mPaint2;
public TestView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mPaint1 = generatePaint(Color.RED, Paint.Style.FILL, 50);
mPaint2 = generatePaint(0x7EFFFF00, Paint.Style.FILL, 50);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawCircle(200, 200, 150, mPaint1);
canvas.drawCircle(200, 200, 100, mPaint2);
}
private Paint generatePaint(int color, Paint.Style style, int strokeWidth) {
Paint paint = new Paint();
paint.setColor(color);
paint.setStyle(style);
paint.setStrokeWidth(strokeWidth);
return paint;
}
}

3. setStyle()

1
void setStyle(Style style)

设置填充样式,对于文字和几何图形都有效。style 的取值如下:

  • Paint.Style.FILL:仅填充内部。
  • Paint.Style.FILL_AND_STROKE:填充内部和描边。
  • Paint.Style.STROKE:仅描边。

示例:
依次是 FILL、STROKE、FILL_AND_STROKE

1
2
3
4
5
6
7
8
9
paintF = generatePaint(Color.RED, Paint.Style.FILL, 50);
paintS = generatePaint(Color.RED, Paint.Style.STROKE, 50);
paintFS = generatePaint(Color.RED, Paint.Style.FILL_AND_STROKE, 50);
paintLine = generatePaint(Color.BLACK, Paint.Style.STROKE, 1);
canvas.drawCircle(225, 300, 150, paintF);
canvas.drawCircle(600, 300, 150, paintS);
canvas.drawCircle(1000, 300, 150, paintFS);
canvas.drawLine(225, 150, 1000, 150, paintLine);

4. setStrokeWidth()

1
void setStrokeWidth(float width)

设置描边宽度值,单位是 px。当画笔的 Style 样式是 STROKE、FILL_AND_STROKE 时有效。当 Style 不起作用时,用于设置画笔宽度。

1.1.3 Canvas使用基础

1. 画布背景设置

有三种方法可以实现画布背景设置:

1
2
3
void drawColor(int color)
void drawARGB(int a, int r, int g, int b)
void drawRGB(int r, int g, int b)

drawColor() 函数中参数 color 的取值必须是 8 位的 0xAARRGGBB 样式颜色值。
drawARGB() 函数允许分别传入 A、R、G、B 分量,每个颜色值的取值范围都是 0~255(对应十六进制的 0x00~0xFF),内部会通过这些颜色分量构造出对应的颜色值。 drawRGB() 函数只允许传入 R、G、B 分量,透明度 Alpha 的值取 255。

示例:
将画布默认填充为紫色

1
2
3
canvas.drawColor(0xFFFF00FF);
canvas.drawARGB(0xFF, 0xFF, 0, 0xFF);
canvas.drawRGB(255, 0, 255);

2. 画直线

1
2
void drawLine(float startX, float startY,
float stopX, float stopY, Paint paint)

参数:

  • startX:起始点 X 坐标。
  • startY:起始点 Y 坐标。
  • stopX:终点 X 坐标。
  • stopY:终点 Y 坐标。

示例:
分别设置 Style:FILL、STROKE、FILL_AND_STROKE

1
2
3
mPaintF = generatePaint(Color.RED, Paint.Style.FILL, 50);
mPaintS = generatePaint(Color.RED, Paint.Style.STROKE, 50);
mPaintFS = generatePaint(Color.RED, Paint.Style.FILL_AND_STROKE, 50);

从效果图中可以明显看出,直线的粗细与画笔 Style 是没有关系的。当设置不同的 StrokeWidth 时,效果如下图所示。
width 分别是 :5、25、50

1
2
3
mPaintF = generatePaint(Color.RED, Paint.Style.FILL, 5);
mPaintS = generatePaint(Color.RED, Paint.Style.STROKE, 25);
mPaintFS = generatePaint(Color.RED, Paint.Style.FILL_AND_STROKE, 50);

可见,直线的粗细是与 paint.setStrokeWidth 有直接关系的。所以,一般而言,paint.setStrokeWidth 在 Style 起作用时,用于设置描边宽度;在 Style 不起作用时,用于设置画笔宽度。

3. 多条直线

1
void drawLines(float[] pts, Paint paint)

参数:
pts:点的集合。从下面的代码中可以看到,这里不是形成连接线,而是每两个点形成一条直线,pts 的组织方式 {x1,y1,x2,y2,x3,y3,…}。

示例:
效果图

1
2
3
4
5
6
7
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(5);
float []pts = {10,10,100,100,200,200,400,400};
// onDraw()
canvas.drawLines(pts, mPaint);

上面有 4 个点,分别是(10,10)、(100,100)、(200,200)和(400,400),两两连成一条直线。

另一个构造函数:

1
void drawLines(float[] pts, int offset, int count, Paint paint)

相比上面的构造函数,这里多了两个参数。

  • int offset:集合中跳过的数值个数。注意不是点的个数!一个点有两个数值。
  • int count:参与绘制的数值个数,指 pts 数组中数值的个数,而不是点的个数,因为一个点有两个数值。
1
2
float[] pts = {10,10,100,100,200,200,400,400};
canvas.drawLines(pts, 2, 4, paint);

表示从 pts 数组中索引为 2 的数字开始绘图,有 4 个数值参与绘图,也就是点(100,100) 和(200,200),所以效果图就是这两个点的连线。

4. 点

1
void drawPoint(float x, float y, Paint paint)

示例:
效果图

1
2
3
4
5
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(15);
canvas.drawPoint(100, 100, mPaint);

在(100,100)位置画一个点。同样,点的大小只与 paint.setStrokeWidth(width) 有关,而与 paint.setStyle 无关。

5. 多个点

1
2
3
void drawPoints(@Size(multiple = 2) @NonNull float[] pts, @NonNull Paint paint)
void drawPoints(@Size(multiple = 2) float[] pts, int offset, int count,
@NonNull Paint paint)

这几个参数的含义与多条直线中的参数含义相同。

  • float[] pts:点的合集,与上面的直线一致,样式为{x1,y1,x2,y2,x3,y3,…}。
  • int offset:集合中跳过的数值个数。注意不是点的个数!一个点有两个数值。
  • int count:参与绘制的数值个数,指 pts 数组中数值的个数,而不是点的个数。

示例:
效果图

1
2
3
4
5
6
7
8
private Paint mPaint;
float[] pts = {10,10, 100,100, 200,200, 400,400};
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(15);
canvas.drawPoints(pts, 2, 4, mPaint);

同样是上面的 4 个点:(10,10)、(100,100)、(200,200)和(400,400),在 drawPoints()函数里跳过前两个数值,即第一个点的横、纵坐标,画出后面 4 个数值代表的点,即第二、三个点,第四个点没画。

6. RectF & Rect

这两个类都是矩形工具类,根据 4 个点构造出一个矩形结构。RectF 与 Rect 中的方法、 成员变量完全一样,唯一不同的是:RectF 是用来保存 float 类型数值的矩形结构的;而 Rect 是用来保存 int 类型数值的矩形结构的。

1
2
3
4
5
6
7
8
9
10
// RectF 的构造函数有如下 4 个,但最常用的还是第二个
RectF()
RectF(float left, float top, float right, float bottom)
RectF(RectF r)
RectF(Rect r)
// Rect 的构造函数有如下 3 个
Rect()
Rect(int left, int top, int right, int bottom)
Rect(Rect r)

一般而言,要构造一个矩形结构,可以通过以下两种方法来实现。

1
2
3
4
5
// 方法一:直接构造
Rect rect = new Rect(10,10,100,100);
// 方法二:间接构造
Rect rect = new Rect();
rect.set(10,10,100,100);

7. 矩形

1
2
3
void drawRect(float left, float top, float right, float bottom, Paint paint)
void drawRect(RectF rect, Paint paint)
void drawRect(Rect r, Paint paint)

第一个函数是直接传入矩形的 4 个点来绘制矩形的;第二、三个函数是根据传入 RectF 或者 Rect 的矩形变量来指定所绘制的矩形的。

示例:
效果图

1
2
3
4
5
6
7
8
mPaintS = generatePaint(Color.RED, Paint.Style.STROKE, 15);
mPaintF = generatePaint(Color.RED, Paint.Style.FILL, 15);
mRect = new RectF(210f, 10f, 300f, 100f);
// 直接构造
canvas.drawRect(10, 10, 100, 100, mPaintS);
// 使用 RectF 构造
canvas.drawRect(mRect, mPaintF);

8. 圆角矩形

1
void drawRoundRect(RectF rect, float rx, float ry, Paint paint)

参数:

  • RectF rect:要绘制的矩形。
  • float rx:生成圆角的椭圆的 X 轴半径。
  • float ry:生成圆角的椭圆的 Y 轴半径。

示例:
圆角矩形效果图

1
2
3
4
mPaintF = generatePaint(Color.RED, Paint.Style.FILL, 15);
mRect = new RectF(100, 110, 300, 200);
canvas.drawRoundRect(mRect, 20, 10, mPaintF);

9. 圆形

1
void drawCircle(float cx, float cy, float radius, Paint paint)

参数:

  • float cx:圆心点的 X 轴坐标。
  • float cy:圆心点的 Y 轴坐标。
  • float radius:圆的半径。

10. 椭圆

1
void drawOval(RectF oval, Paint paint)

参数:
RectF oval:用来生成椭圆的矩形。

椭圆是根据矩形生成的,以矩形的长为椭圆的 X 轴,以矩形的宽为椭圆的 Y 轴。

示例:
使用矩形画椭圆

1
2
3
4
5
6
mPaintO = generatePaint(Color.RED, Paint.Style.STROKE, 5);
mPaintR = generatePaint(Color.BLUE, Paint.Style.STROKE, 5);
mRect = new RectF(100, 110, 300, 200);
canvas.drawRect(mRect, mPaintR); // 画矩形
canvas.drawOval(mRect, mPaintO); // 画椭圆

11. 弧

1
2
void drawArc(RectF oval, float startAngle, float sweepAngle,
boolean useCenter, Paint paint)

参数:

  • RectF oval:生成椭圆的矩形。
  • float startAngle:弧开始的角度,以 X 轴正方向为 0°。
  • float sweepAngle:弧持续的角度。
  • boolean useCenter:是否有弧的两边。为 true 时,表示带有两边;为 false 时,只有一条弧。

示例:
弧 效果图

1
2
3
4
5
6
7
8
mPaint = generatePaint(Color.RED, Paint.Style.STROKE, 5);
mRect1 = new RectF(100, 100, 200, 200);
mRect2 = new RectF(220, 100, 320, 200);
// 带弧的两边
canvas.drawArc(mRect1, 0, 90, true, mPaint);
// 不带弧的两边
canvas.drawArc(mRect2, 0, 90, false, mPaint);

上述代码中,仅将 paint 的样式设置为 FILL 。效果图如下:
填充样式的弧

当画笔设为填充模式时,填充区域只限于圆弧的起始点和终点所形成的区域。当带有两边时,会将两边及圆弧内部全部填充;如果没有两边,则只填充圆弧部分。

1.1.4 Rect与RectF

1.是否包含点、矩形

1)判断是否包含某个点

1
boolean contains(int x, int y)

该函数用于判断某个点是否在当前矩形中。如果在,则返回 true;如果不在,则返回 false。 参数(x,y)就是当前要判断的点的坐标。

示例:绘制一个灰色矩形,当手指在这个矩形区域内时,矩形变为红色。
效果图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class TestView extends View {
private float mX, mY;
private Paint mPaint;
private RectF mRect;
public TestView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mPaint = new Paint();
mPaint.setStyle(Paint.Style.FILL);
mRect = new RectF(100, 100, 500, 350);
}
@Override
protected void onDraw(Canvas canvas) {
if (mRect.contains(mX, mY)) {
mPaint.setColor(Color.RED);
} else {
mPaint.setColor(Color.GRAY);
}
canvas.drawRect(mRect, mPaint);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mX = event.getX();
mY = event.getY();
if (event.getAction() == MotionEvent.ACTION_DOWN) {
invalidate();
return true;
} else if (event.getAction() == MotionEvent.ACTION_UP) {
mX = -1;
mY = -1;
}
invalidate();
return super.onTouchEvent(event);
}
}

上述代码注意两点:

  • 在 MotionEvent.ACTION_DOWN 中返回 true,因为当 MotionEvent. ACTION_DOWN 消息到来时,系统会判断返回值,当返回 true 时,表示当前控件已经在拦截 (消费)这个消息了,所以后续的 ACTION_MOVE、ACTION_UP 消息仍然继续传过来。如果返回 false(系统默认返回 false),就表示当前控件不需要这个消息,那么后续的 ACTION_MOVE、ACTION_UP 消息就不会再传到这个控件。

  • postInvalidate() 和 invalidate() 函数都是用来重绘控件的,区别是 invalidate() 函数一定要在主线程中执行,否则就会报错;而 postInvalidate() 函数则没有那么多讲究,它可以在任何线程中执行,而不必一定是主线程。因为在 postInvalidate() 函数中就是利用 handler 给主线程发送刷新界面的消息来实现的,所以它可以在任何线程中执行而不会出错。而正因为它是通过发送消息来实现的,所以它的界面刷新速度可能没有直接调用 invalidate() 函数那么快。确定当前线程是主线程的情况下,以 invalidate() 函数为主。否则调用调用 postInvalidate() 函数为好。因为 onTouchEvent() 函数本来就是在主线程中的,所以使用 invalidate() 函数更合适。

2)判断是否包含某个矩形

1
2
3
// 根据矩形的 4 个点或者一个 Rect 矩形对象来判断这个矩形是否在当前的矩形区域内。
boolean contains(float left, float top, float right, float bottom)
boolean contains(RectF r)

2.判断两个矩形是否相交

1)静态方法判断是否相交

1
static boolean intersects(Rect a, Rect b)

这是 Rect 类的一个静态方法,用来判断参数中所传入的两个 Rect 矩形是否相交,如果相交则返回 true,否则返回 false。
2)成员方法判断是否相交
判断当前 Rect 对象与其他矩形是否相交。

1
boolean intersects(int left, int top, int right, int bottom)

使用方法:

1
2
Rect rect_1 = new Rect(10,10,200,200);
boolean interset1_2 = rect_1.intersects(190, 10, 250, 200);

3)判断相交并返回结果

1
2
boolean intersect(int left, int top, int right, int bottom)
boolean intersect(Rect r)

这两个成员方法与 intersects()方法的区别是,不仅会返回是否相交的结果,而且会把相交部分的矩形赋给当前 Rect 对象。如果两个矩形不相交,则当前 Rect 对象的值不变。

3.合并

1)合并两个矩形
合并两个矩形的意思就是将两个矩形合并成一个矩形,即无论这两个矩形是否相交,取两个矩形最小左上角点作为结果矩形的左上角点,取两个矩形最大右下角点作为结果矩形的右下角点。如果要合并的两个矩形有一方为空,则将有值的一方作为最终结果。

1
2
public void union(int left, int top, int right, int bottom)
public void union(Rect r)

示例:
绿色、红色矩形合并成蓝色矩形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mRect1 = new RectF(200, 100, 500, 300);
mRect2 = new RectF(100, 200, 300, 400);
// 画出右上的红色矩形
mPaint.setColor(Color.RED);
canvas.drawRect(mRect1, mPaint);
// 画出左下的绿色矩形
mPaint.setColor(Color.GREEN);
canvas.drawRect(mRect2, mPaint);
// 矩形合并
mRect1.union(mRect2);
// 画出合并后的矩形(蓝色部分)
mPaint.setColor(Color.BLUE);
canvas.drawRect(mRect1, mPaint);

2)合并矩形与某个点

1
public void union(int x, int y)

先判断当前矩形与目标合并点的关系。如果不相交,则根据目标点(x,y)的位置,将目标点设置为当前矩形的左上角点或者右下角点。如果当前矩形是一个空矩形,则最后的结果矩形为([0,0],[x,y]),即结果矩形的左上角点为[0,0],右下角点为[x,y]。

1.1.5 Color

Color 是 Android 中与颜色处理有关的类。

1. 常量颜色

1
2
3
4
5
6
7
8
9
10
11
12
@ColorInt public static final int BLACK = 0xFF000000;
@ColorInt public static final int DKGRAY = 0xFF444444;
@ColorInt public static final int GRAY = 0xFF888888;
@ColorInt public static final int LTGRAY = 0xFFCCCCCC;
@ColorInt public static final int WHITE = 0xFFFFFFFF;
@ColorInt public static final int RED = 0xFFFF0000;
@ColorInt public static final int GREEN = 0xFF00FF00;
@ColorInt public static final int BLUE = 0xFF0000FF;
@ColorInt public static final int YELLOW = 0xFFFFFF00;
@ColorInt public static final int CYAN = 0xFF00FFFF;
@ColorInt public static final int MAGENTA = 0xFFFF00FF;
@ColorInt public static final int TRANSPARENT = 0;

Color.XXX 来直接使用这些颜色,比如红色:Color.RED。

2. 构造颜色

1
2
3
4
// 带有透明度的颜色
static int argb(int alpha, int red, int green, int blue)
// 不带透明度的颜色:alpha 值取 255
static int rgb(int red, int green, int blue)

argb() 函数的源码:

1
2
3
4
public static int argb(int alpha, int red, int green, int blue) {
// 位运算,值得借鉴
return (alpha << 24) | (red << 16) | (green << 8) | blue;
}

3. 提取颜色分量

1
2
3
4
static int alpha(int color)
static int red(int color)
static int green(int color)
static int blue(int color)

通过上面的 4 个函数提取出对应的 A、R、G、B 颜色分量。

1
2
// 得到的结果 green 的值就是 0x0F
int green = Color.green(0xFF000F00);